/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.uml_generator.manager.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

import com.genia.toolbox.basics.bean.DisplayObject;
import com.genia.toolbox.basics.bean.impl.ConstantDelimiter;
import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.uml_generator.Constants;
import com.genia.toolbox.uml_generator.bean.ModelGeneratorContext;
import com.genia.toolbox.uml_generator.manager.ModelGenerator;
import com.genia.toolbox.uml_generator.model.AbstractClassModel;
import com.genia.toolbox.uml_generator.model.AbstractJavaObject;
import com.genia.toolbox.uml_generator.model.AttributeModel;
import com.genia.toolbox.uml_generator.model.DataTypeModel;
import com.genia.toolbox.uml_generator.model.OperationModel;

/**
 * manager to generate the model of the interfaces from the UML datas.
 */
public class ClassGeneratorImpl
    extends AbstractManager
    implements ModelGenerator
{

  /**
   * the name of the variable that will contain the cashed hashcode.
   */
  private static final String HASHCODE_CASH_VARIABLE_NAME = "cashedHashCode";

  /**
   * the name of the private field containing the cash {@link ThreadLocal} for
   * recursive equality.
   */
  private static final String EQUALITY_CASH_VARIABLE_NAME = "EQUALITY_CASH";

  /**
   * the name of the variable containing the cash {@link Set} for recursive
   * equality.
   */
  private static final String CASH_SET_VARIABLE_NAME = "cashSet";

  /**
   * the name of the variable containing the
   * {@link com.genia.toolbox.basics.bean.Pair} for recursive equality.
   */
  private static final String PAIR_VARIABLE_NAME = "cashPair";



  /**
   * the generator.
   * 
   * @param context
   *          the context of the generation
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.ModelGenerator#generate(com.genia.toolbox.uml_generator.bean.ModelGeneratorContext)
   */
  public void generate(ModelGeneratorContext context)
      throws BundledException
  {
    for (AbstractClassModel classModel : getCollectionManager().filterList(context.getClasses().values(), Constants.ONLY_GENERATABLE_CLASSES)) {
      generateClass(context, classModel);
      getDelegateManager().generateDelegate(context, classModel);
    }
  }



  /**
   * generate the interface.
   * 
   * @param context
   *          the context of the generation
   * @param classModel
   *          the model of the interface to generate.
   * @throws BundledException
   *           when an error occurred
   */
  private void generateClass(ModelGeneratorContext context, AbstractClassModel classModel)
      throws BundledException
  {
    File classFile = new File(context.getDirectoryForPackage(context.getConfiguration().getGeneratedSrcDir(), classModel), classModel.getName() + ".java");
    classFile.getParentFile().mkdirs();
    try {
      PrintWriter out = new PrintWriter(new FileOutputStream(classFile));
      context.getGeneratedSources().add(classFile);

      getPrinterManager().printPackage(out, classModel).println();

      getPrinterManager().printComment(out, classModel.getComment(), Constants.COMMENT_WARNING_GENERATED);
      out.append("public ");
      if (classModel.isAbstractClass()) {
        out.append("abstract ");
      }
      out.append("class ").append(classModel.getName()).println();
      getPrinterManager().printDependencies(context, out, classModel).append("{");

      getDelegateManager().printDelegateCall(out, classModel.getUndecoraredObject());

      for (AttributeModel attribute : getAttributeManager().newAttributes(classModel)) {
        getPrinterManager().printAttribute(out, attribute);
        getPrinterManager().printMethod(out, getAttributeManager().getGetter(attribute));
        getPrinterManager().printMethod(out, getAttributeManager().getSetter(attribute));
      }

      for (OperationModel operation : getAssociationManager().getAccessorMethodForAssociations(getAssociationManager().newAssociations(classModel))) {
        getPrinterManager().printMethod(out, operation);
      }

      generateHashCode(out, classModel);
      generateEquals(out, classModel);

      out.append("}");
      out.close();
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * <p>
   * </p>
   */

  /**
   * generate the equals method.
   * 
   * @param out
   *          the {@link PrintWriter} to write to
   * @param classModel
   *          the {@link AbstractClassModel} to generate the equals for
   * @throws BundledException
   *           when an error occurred
   */
  private void generateEquals(PrintWriter out, AbstractClassModel classModel)
      throws BundledException
  {
    final Set<AttributeModel> attributes = getAttributeManager().removeIdentifier(classModel);
    boolean needCash = !getCollectionManager().filterSet(attributes, Constants.REMOVE_MAPPABLE_PREDICATE).isEmpty();
    // We need a way to stock already tried equality because of recursive
    // calls
    final DataTypeModel pairModel = getModelDecorator().getGenericObject(Constants.REFERENCE_UN_ORDERED_PAIR_MODEL, classModel, classModel);
    final AbstractJavaObject cashSet = getModelDecorator().getGenericObject(Constants.SET_MODEL, pairModel);
    final AbstractJavaObject casThreadLocalModel = getModelDecorator().getGenericObject(Constants.THREAD_LOCAL_MODEL, cashSet);
    if (needCash) {
      getPrinterManager().printComment(out, "the variable that allow to do break recursive equality called.");
      out.append("private static final ").append(casThreadLocalModel.getQualifiedName()).append(" ").append(EQUALITY_CASH_VARIABLE_NAME).append(" = ").append(casThreadLocalModel.getDefaultValue())
          .append(";\n");
    }

    getPrinterManager().printComment(out, "Indicates whether some other object is \"equal to\" this one.", "@param obj the reference object with which to compare.",
        "@return <code>true</code> if this object is the same as the obj argument; <code>false</code> otherwise.", "@see #hashCode()", "@see java.util.Hashtable");
    out.append("@Override\n");
    out.append("public boolean equals(final Object obj)\n{\n");
    out.append("if(this == obj) { return true; }\n");
    out.append("if(obj == null) { return false; }\n");
    out.append("if(!").append(Constants.PROXY_HELPER_MODEL.getQualifiedName()).append(".compareClasses(getClass(), obj.getClass())) { return false; }\n");
    out.append("final ").append(classModel.getName()).append(" other = (").append(classModel.getName()).append(") obj;\n");
    if (getModelInspector().isPersistable(classModel)) {
      out.append("if(!").append(Constants.URL_OBJECT_UTILS).append(".nullSafeEquals(getIdentifier(), other.getIdentifier())) { return false; }\n");
      out.append("if(getIdentifier() != null) { return true; }\n");
    }
    if (classModel.getParent() != null) {
      out.append("if(!super.equals(obj)) { return false; }\n");
    }
    if (needCash) {
      out.append(cashSet.getQualifiedName()).append(" ").append(CASH_SET_VARIABLE_NAME).append(" = ").append(EQUALITY_CASH_VARIABLE_NAME).append(".get();\n");
      out.append("if (").append(CASH_SET_VARIABLE_NAME).append(" == null) { ").append(CASH_SET_VARIABLE_NAME).append(" = ").append(cashSet.getDefaultValue()).append(";").append(
          EQUALITY_CASH_VARIABLE_NAME).append(".set(").append(CASH_SET_VARIABLE_NAME).append(");}\n");
      out.append("final ").append(pairModel.getQualifiedName()).append(" ").append(PAIR_VARIABLE_NAME).append(" = ").append(pairModel.getDefaultValue()).append(";\n");
      out.append(PAIR_VARIABLE_NAME).append(".setFirstElement(this);");
      out.append(PAIR_VARIABLE_NAME).append(".setSecondElement(other);");
      out.append("if(").append(CASH_SET_VARIABLE_NAME).append(".contains(").append(PAIR_VARIABLE_NAME).append(")) { return true; }\n");
      out.append(CASH_SET_VARIABLE_NAME).append(".add(").append(PAIR_VARIABLE_NAME).append(");\ntry {\n");
    }
    for (AttributeModel attribute : attributes) {
      String getter = getAttributeManager().getGetter(attribute).getName() + "()";
      out.append("if(!").append(Constants.URL_OBJECT_UTILS).append(".nullSafeEquals(").append(getter).append(",other.").append(getter).append(")) { return false; }\n");
    }
    if (needCash) {
      out.append("} finally {\n");
      out.append(CASH_SET_VARIABLE_NAME).append(".remove(").append(PAIR_VARIABLE_NAME).append(");\n");
      out.append("}\n");
    }
    out.append("return true;\n}\n");
  }



  /**
   * generate the hashCode method.
   * 
   * @param out
   *          the {@link PrintWriter} to write to
   * @param classModel
   *          the {@link AbstractClassModel} to generate the hashCode for
   * @throws BundledException
   *           when an error occurred
   */
  private void generateHashCode(PrintWriter out, AbstractClassModel classModel)
      throws BundledException
  {
    getPrinterManager().printComment(out, "cashed value for the hashCode.");
    if (getModelInspector().isPersistable(classModel)) {
      out.append("private transient Integer ").append(HASHCODE_CASH_VARIABLE_NAME).append(" = null;");
    }
    getPrinterManager().printComment(out, "Returns a hash code value for the object. This method is supported for the benefit of hashtables such as those provided by {@link java.util.Hashtable}.",
        "@return a hash code value for this object.", " @see java.lang.Object#equals(java.lang.Object)", "@see java.util.Hashtable");
    out.append("@Override\n");
    out.append("@SuppressWarnings(\"unchecked\")\n");
    out.append("public int hashCode()\n{\n");
    if (getModelInspector().isPersistable(classModel)) {
      out.append("if (").append(HASHCODE_CASH_VARIABLE_NAME).append(" == null) {\n");
      out.append("").append(HASHCODE_CASH_VARIABLE_NAME).append(" = ").append(Constants.URL_OBJECT_UTILS).append(".nullSafeHashCode(getIdentifier());");
      out.append("}\n");
      out.append("return ").append(HASHCODE_CASH_VARIABLE_NAME).append(";\n");
    }
    else {
      Set<AttributeModel> attributes = getCollectionManager().filterSet(getAttributeManager().removeIdentifier(classModel), Constants.REMOVE_UN_MAPPABLE_PREDICATE);
      if (attributes.isEmpty()) {
        if (classModel.getParent() != null) {
          out.append("return super.hashCode();");
        }
        else {
          out.append("return 0;");
        }
      }
      else {
        out.append("return ").append(Constants.URL_OBJECT_UTILS).append(".nullSafeHashCode(").append(Constants.URL_ARRAYS).append(".asList(");
        String prefix = "";
        if (classModel.getParent() != null) {
          prefix = "super.hashCode(),";
        }
        getCollectionManager().join(out, attributes, new ConstantDelimiter(prefix, ",", "));\n"), new DisplayObject<AttributeModel>()
        {
          public void append(Appendable appendable, AttributeModel object)
              throws IOException
          {
            appendable.append(getAttributeManager().getGetter(object).getName()).append("()");
          }
        });
      }
    }
    out.append("}\n");
  }

}
